package com.magenta.maxoptra.gis.service;

import com.magenta.maxoptra.cache.CachedOnWriteTable;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.commons.StringUtils;
import com.magenta.maxoptra.gis.util.Apis;
import com.magenta.maxoptra.gis.utils.Coordinates;
import org.apache.commons.lang.BooleanUtils;
import org.jetbrains.annotations.Nullable;

import javax.annotation.Resource;
import javax.ejb.*;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import static com.magenta.maxoptra.gis.GisParameter.enableCache;

/**
 * <p/>
 * <b>WARNING!!!</b> now we are caching only best search result!
 *
 * @author ivictus
 */
@Local({SyncGeocodingService.class, SyncReverseGeocodingService.class})
//@Remote({SyncGeocodingServiceRemote.class, SyncReverseGeocodingServiceRemote.class})
public class SyncGeocodingServiceImpl implements SyncGeocodingService, SyncReverseGeocodingService, AddressesCacheMBean {

    private static final String SCHEMA_NAME = "routes_cache";
    private static final String DS_JNDI = "java:/maxoptra/datasource/routes-cache";

    protected Logger log = new Logger(getClass());

    @Resource(mappedName = DS_JNDI, type = DataSource.class)
    private DataSource ds;

    @EJB
    GeoProviderFacade geoProviderFacade;

    @EJB
    ProvidersRegistry geoProvidersRegistry;

    private boolean isCacheEnabled(GeoProviderSettings settings) {
        if (settings != null && settings.getGisParams() != null) {
            String enabled = settings.getGisParam(enableCache.withProvider(settings.getProviderName().toLowerCase()));
            return StringUtils.isEmpty(enabled) || BooleanUtils.toBoolean(enabled);
        }
        return true;
    }

    private static CachedOnWriteTable<ForwardGoecodingPK, GeoAddress> geocodeRequests;
    private static CachedOnWriteTable<ReverseGeocodingPK, GeoAddress> reverseGeocodeRequests;


    
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public List<GeoAddress> getAddress(GeoProviderSettings settings, String query) throws GeoException {
        if (!isValidQuery(query)) {
            return Collections.emptyList();
        }
        String providerName = settings.getProviderName();
        String versionApi = settings.getVersionApi();
        Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
        ForwardGoecodingPK queryPK = new ForwardGoecodingPK(cid, query);
        boolean enabledCache = isCacheEnabled(settings);
        if (enabledCache) {

            GeoAddress cachedAddress = null;
            try {
                cachedAddress = geocodeRequests.get(queryPK);
                if (cachedAddress != null) {
                    List<GeoAddress> addresses = new ArrayList<GeoAddress>();
                    while (cachedAddress != null) {
                        addresses.add(cachedAddress);
                        queryPK.index++;
                        cachedAddress = geocodeRequests.get(queryPK);
                    }
                    return addresses;
                }
            } catch (Exception e) {
                log.debug("REQUEST OF ADDRESSES CACHE FAILED!", e);
            }
        }

        IGeocoding codingService = geoProviderFacade.getGeoCoder(settings);
        List<GeoAddress> addresses = new ArrayList<GeoAddress>();
        List<GeoAddress> addressByKeyword = null;
        addressByKeyword = codingService.findAddressByKeyword(settings, query, 10);
        addresses.addAll(addressByKeyword);
        if (enabledCache) {
            try {
                for (GeoAddress address : addresses) {
                    geocodeRequests.put(queryPK, address);
                    queryPK.index++;
                    geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                }
            } catch (Exception e) {
                log.debug("PERSIST IN ADDRESSES CACHE FAILED!", e);
            }
        }
        return addresses;
    }

    private boolean isValidQuery(String queryString) {
        return queryString != null && !queryString.isEmpty();
    }


    
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public GeoAddress getCountryAddress(GeoProviderSettings settings, String country) {
        if (isValidQuery(country)) {
            String providerName = settings.getProviderName();
            String versionApi = settings.getVersionApi();
            Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
            ForwardGoecodingPK queryPK = new ForwardGoecodingPK(cid, country);
            boolean enabledCache = isCacheEnabled(settings);
            if (enabledCache) {
                GeoAddress cachedAddress = null;
                try {
                    cachedAddress = geocodeRequests.get(queryPK);
                    if (cachedAddress != null) {
                        return cachedAddress;
                    }
                } catch (Exception e) {
                    log.error("REQUEST OF ADDRESSES CACHE FAILED!", e);
                }
            }

            IGeocoding codingService = geoProviderFacade.getGeoCoder(settings);
            try {
                GeoAddress requestedAddress = codingService.findBestAddressByKeyword(settings, country);
                putAddress(enabledCache, requestedAddress, queryPK);
                geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                return requestedAddress;
            } catch (GeoException e) {
                log.warnX(e.getLocalizedMessage(), e);
                throw new IllegalStateException(e);
            }
        }
        return null;
    }

    private void putAddress(boolean enabledCache, GeoAddress requestedAddress, ForwardGoecodingPK queryPK) {
        if (enabledCache && requestedAddress != null) {
            try {
                putAddress(enabledCache, requestedAddress, queryPK);
            } catch (Exception e) {
                log.error("PERSIST IN ADDRESSES CACHE FAILED!", e);
            }
        }
    }

    
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public GeoAddress getBestAddress(GeoProviderSettings settings, String query) {
        if (isValidQuery(query)) {
            String providerName = settings.getProviderName();
            String versionApi = settings.getVersionApi();
            Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
            ForwardGoecodingPK queryPK = new ForwardGoecodingPK(cid, query);
            boolean enabledCache = isCacheEnabled(settings);
            if (enabledCache) {
                GeoAddress cachedAddress = null;
                try {
                    cachedAddress = geocodeRequests.get(queryPK);
                    if (cachedAddress != null) {
                        return cachedAddress;
                    }
                } catch (Exception e) {
                    log.error("REQUEST OF ADDRESSES CACHE FAILED!", e);
                }
            }

            IGeocoding codingService = geoProviderFacade.getGeoCoder(settings);
            try {
                GeoAddress requestedAddress = codingService.findBestAddressByKeyword(settings, query);
                if (enabledCache && requestedAddress != null) {
                    try {
                        geocodeRequests.put(queryPK, requestedAddress);
                        geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                    } catch (Exception e) {
                        log.error("PERSIST IN ADDRESSES CACHE FAILED!", e);
                    }
                }
                return requestedAddress;
            } catch (GeoException e) {
                log.warnX(e.getLocalizedMessage(), e);
                throw new IllegalStateException(e);
            }
        }
        return null;
    }

    
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address) {
        IGeocoding codingService = geoProviderFacade.getGeoCoder(settings);
        try {
            return codingService.doForwardGeocoding(settings, address, 100);
        } catch (GeoException e) {
            log.warnX(e.getLocalizedMessage(), e);
            throw new IllegalStateException(e);
        }
    }

    @Override
    public GeoAddress getBestAddress(GeoProviderSettings settings, GeoAddress address) {
        IGeocoding codingService = geoProviderFacade.getGeoCoder(settings);
        try {
            List<GeoAddress> addresses = codingService.doForwardGeocoding(settings, address, 1);
            if (addresses!= null && addresses.size() > 0) {
                return addresses.get(0);
            }
        } catch (GeoException e) {
            log.warnX(e.getLocalizedMessage(), e);
        }
        return null;
    }

    @Override
    public long cleanAndGetDateOfFirstOldest(final long cid, final long maxAge) {
        final long date = System.currentTimeMillis() - maxAge;
        try {
            geocodeRequests.removeBeforeDate(cid, date);
            reverseGeocodeRequests.removeBeforeDate(cid, date);
            return Math.min(geocodeRequests.getDateOfFirstOldest(cid), reverseGeocodeRequests.getDateOfFirstOldest(cid));
        } catch (SQLException e) {
            log.error("Can't clean caches of geocode and reverse geocode requests", e);
        }
        return Long.MAX_VALUE;
    }


    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public GeoAddress findBestAddressByCoords(GeoProviderSettings settings, GeographicCoordinates geoCoordinates) {
        if (isValidCoordinates(geoCoordinates)) {
            String providerName = settings.getProviderName();
            String versionApi = settings.getVersionApi();
            Long cid = Apis.cid(providerName, versionApi, settings.getCidDiscriminator());
            ReverseGeocodingPK queryPK = new ReverseGeocodingPK(providerName, versionApi, settings.getCidDiscriminator(), geoCoordinates);
            boolean enabledCache = isCacheEnabled(settings);
            if (enabledCache) {
                GeoAddress cachedAddress = null;
                try {
                    cachedAddress = reverseGeocodeRequests.get(queryPK);
                    if (cachedAddress != null) {
                        return cachedAddress;
                    }
                } catch (Exception e) {
                    log.debug("REQUEST OF ADDRESSES2 CACHE FAILED!", e);
                }
            }
            IReverseGeocoding reverseGeocodingService = geoProviderFacade.getReverseGeocodingProvider(settings);
            try {
                GeoAddress requestedAddress = reverseGeocodingService.doReverseGeocoding(geoCoordinates);
                if (enabledCache) {
                    try {
                        if (requestedAddress != null) {
                            reverseGeocodeRequests.put(queryPK, requestedAddress);
                            geoProvidersRegistry.assignCID(cid, providerName, versionApi, settings.getCidDiscriminator());
                        }
                    } catch (Exception e) {
                        log.debug("PERSIST IN ADDRESSES2 CACHE FAILED!", e);
                    }
                }
                return requestedAddress;
            } catch (GeoException e) {
                log.warnX(e.getLocalizedMessage(), e);
                throw new IllegalStateException(e);
            }
        }
        return null;
    }

    private boolean isValidCoordinates(GeographicCoordinates geographicCoordinates) {
        //TODO: implementation
        return true;
    }

    
    public void stop() {
        // TODO Auto-generated method stub

    }

    
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public void start() {
        init();
    }


    public void init() {
        log.info("DATA %s SOURCE INITIALIZED.", DS_JNDI);
        initGeocodeCache();
        initReverseGeocodeCache();
    }

    private void initGeocodeCache() {
        String tableName = "forward_geocoding";
        ForwardGoecodingPK initKey = new ForwardGoecodingPK(0L, "*");
        GeoAddress initValue = new GeoAddress();
        synchronized (java.lang.Class.class) {
            try {
                String schemaName = ds != null ? ds.getConnection().getCatalog() : SCHEMA_NAME;
                if (geocodeRequests == null) {
                    geocodeRequests = new CachedOnWriteTable<ForwardGoecodingPK, GeoAddress>(
                            schemaName, tableName, ds, log, initKey, initValue, "timestamp");
                }
                log.info("CACHED TABLE %s INITIALIZED!", tableName);
            } catch (Exception e) {
                log.error(e);
            }
        }
    }

    private void initReverseGeocodeCache() {
        String tableName = "reverse_geocoding";
        ReverseGeocodingPK initKey = new ReverseGeocodingPK(0L, new Coordinates(0d, 0d));
        GeoAddress initValue = new GeoAddress();
        initValue.setLatitude(0d);
        initValue.setLongitude(0d);
        synchronized (java.lang.Class.class) {
            try {
                String schemaName = ds != null ? ds.getConnection().getCatalog() : SCHEMA_NAME;
                if (reverseGeocodeRequests == null) {
                    reverseGeocodeRequests = new CachedOnWriteTable<ReverseGeocodingPK, GeoAddress>(
                            schemaName, tableName, ds, log, initKey, initValue, "timestamp");
                }
                log.info("CACHED TABLE %s INITIALIZED!", tableName);
            } catch (Exception e) {
                log.error(e);
            }
        }
    }

}
